/*
 * приложение для обмена текстовыми сообщениями с интерфейсом командной строки.
 * сделано на основе библиотеки zeromq.
 * скомпилировано в msvc 2010.
 * запуск без параметров печатает на экран справку.
 * "q" - выход из приложения
 */

#include "stdafx.h"

#include <string>
#include <iostream>
#include <memory>
#include <fstream>
#include <map>
#include <zmq.hpp>
#include <windows.h>
#include <process.h>

using namespace std;
using namespace zmq;

const string str_file = "-f";
const string str_addr = "-a";
const string str_rmtp = "-r";
const string str_lclp = "-l";

const string key_server_addr = "server";
const string key_server_port = "server_port";
const string key_client_port = "client_port";

/**
 * Класс загрузки настроек из командной строки либо из файла.
 *
 * Формат файла настроек такой:
 * ключ1=значение1
 * ...
 * ключН=значениеН
 */
class Config {
public:
	/**
	 * считывание файла и загрузка пар ключ-значение в память
	 * @param argc количество параметров (из main)
	 * @param argv массив параметров (из main)
	 */
	Config(int argc, char* argv[]);
	/**
	 * печать считанной конфигурации на экран
	 */
	void printConfig();
	/**
	 * получение значения параметра по ключу
	 * @param key ключ для поиска
	 * @return Возвращает значение ключа либо пусто, если ничего не найдено
	 */
	const string& getValue(const string& key);
private:
	/**
	 * получение значения параметра по ключу
	 */
	map<string, string> prop;
	/**
	 * разбор параметров командной строки.
	 * доступные значения:
	 * -f{имя файла}
	 * -a{адрес партнера}
	 * -r{порт партнера - для получения данных}
	 * -l{порт свой - для рассылки данных}
	 * @param argc количество параметров (из main)
	 * @param argv массив параметров (из main)
	 */
	void parseOptions(int argc, char* argv[]) {
		string filename;
		for (int i = 1; i < argc; i++) {
			//cout << i << "." << argv[i] << "\n";
			string option = (string)argv[i];
			if (option.find(str_file) == 0) {
				filename = option.substr(2);
			} else if (option.find(str_addr) == 0) {
				prop[key_server_addr] = option.substr(2);
			} else if (option.find(str_rmtp) == 0) {
				prop[key_server_port] = option.substr(2);
			} else if (option.find(str_lclp) == 0) {
				prop[key_client_port] = option.substr(2);
			}
		}
		if (!filename.empty())
			readFile(filename);
	}
	/**
	 * считывание файла и загрузка пар ключ-значение в память.
	 * @param filename имя файла с параметрами
	 */
	void readFile(const string& filename);
};
/**
 * @see Config#Config
 */
Config::Config(int argc, char* argv[]) {
	parseOptions(argc, argv);
	printConfig();
}
/**
 * @see Config#printConfig
 */
void Config::printConfig() {
	for (map<string, string>::iterator it = prop.begin(); it != prop.end(); it++ )
		cout << ">" << it->first << "=" << it->second << endl;
}
/**
 * @see Config#getValue
 */
const string& Config::getValue(const string& key) {
	return prop[key];
}
/**
 * @see Config#readFile
 */
void Config::readFile(const string& filename) {
    ifstream file(filename.c_str());
    int count = 0;
    if (file.is_open()) {
		string line;
		// в цикле построчно считать файл и разобрать каждую считанную строку
		while (file.good()) {
			try {
				getline(file, line);
				int pos = line.find('=');
				if (pos != string::npos) {
					string key = line.substr(0, pos);
					string value = line.substr(pos+1);
					prop[key] = value;
				}
			} catch (...) {
				cout << "problem on read file '" << filename << "'!\n";
				break;
			}
		}
		file.close();
    } else
		cout << "can't open file '" << filename << "'!\nit is busy or not exist.\n";
}
/**
 * Главный класс программы, занимающийся рассылкой и приемом сообщений от такого же партнера.
 * настройки получает их файла либо из командной строки
 */
class Chat {
public:
	/*
	 * инициализация внетренних переменных и чтение конфигурации
	 * @param argc количество параметров (из main)
	 * @param argv массив параметров (из main)
	 */
	Chat(int argc, char* argv[]): myThreadID1(1), myThreadID2(2), 
		myHandle1(NULL), myHandle2(NULL),
		context(new context_t(2)), config(argc, argv) {
	}
	/**
	 * установка адресов и портов для соединения
	 */
	void setUrls();
	/**
	 * проверка корректности настроек перед запуском
	 * @return возвращает true если настройки корректны, иначе - false
	 */
	bool checkUrls();
	/**
	 * вывод на экран подсказки для запуска программы
	 * @param name - имя программы
	 */
	void printHelp(const string& name);
	/**
	 * создание и старт потока рассылки сообщений
	 * @return Возвращает дескриптор потока (для синхронизации вне объекта)
	 */
	HANDLE startSender();
	/**
	 * создание и старт потока слушателя входящих сообщений
	 * @return Возвращает дескриптор потока (для синхронизации вне объекта)
	 */
	HANDLE startReciver();
	/**
	 * деструктор закрывает потоки рассылки и слушателя,
	 * удаляет локальные внутренние объекты
	 */
	~Chat();
private:
	/**
	 * конфигурация, считанная из файла
	 */
	Config config;
	/**
	 * конфигурация zeromq
	 */
	context_t* context;
	/*
	 * строка адреса для приема сообщений
	 */
	string recive_url;
	 /*
	  * строка адреса для рассылки сообщений
	  */
	string send_url;
	/**
	 * идентификатор потока рассылки
	 */
	unsigned int myThreadID1;
	/**
	 * идентификатор потока слушателя
	 */
	unsigned int myThreadID2;
	/**
	 * дескриптор потока рассылки (winapi)
	 */
	HANDLE myHandle1;
	/**
	 * дескриптор потока слушателя (winapi)
	 */
	HANDLE myHandle2;
	/**
	 * функция для запуска в потоке слушателя
	 * @param lpParameter указатель на данные,
	 * в котором приходит адрес объекта Chat
	 */
	static unsigned WINAPI reciver(LPVOID lpParameter);
	/**
	 * функция для запуска в потоке рассылки
	 * @param lpParameter указатель на данные,
	 * в котором приходит адрес объекта Chat
	 */
	static unsigned WINAPI sender(LPVOID lpParameter);
	/*
	 * @see Config#getValue
	 */
	const string& getValue(const string& key) {
		return config.getValue(key);
	}
};
/**
 * @see Chat#setUrls
 */
void Chat::setUrls() {
	// общий вид адреса слушателя: "tcp://*:5555"
	recive_url = "tcp://*:" + getValue(key_client_port);
	// общий вид адреса для рассылки "tcp://localhost:5556";
	send_url = "tcp://" + getValue(key_server_addr) + ":" + getValue(key_server_port);
	this->recive_url.assign(recive_url);
	this->send_url.assign(send_url);
	cout << "recive port: " << recive_url << endl 
		<< "send port: " << send_url << endl;
}
/**
 * @see Chat#checkUrls
 */
bool Chat::checkUrls() {
	if (getValue(key_client_port).empty() || 
		getValue(key_server_addr).empty() ||
		getValue(key_server_port).empty())
		return false;
	return true;
}
/**
 * @see Chat#printHelp
 */
void Chat::printHelp(const string& name){
	cout << "================================================" << endl
		<< "text char messenger with command line interface." << endl
		<< "================================================" << endl
		<< "\tusage: " << endl
		<< name << " [-f{inifile}] | [-a{address} -r{remote port} -l{local port}]" << endl
		<< "================================================" << endl
		<< "\tMode 1. -f{inifile}" << endl << "\texample file format (with name 'prop_1'):" << endl
		<< key_server_addr << "=localhost" << endl
		<< key_server_port << "=5555" << endl
		<< key_client_port << "=5556" << endl
		<< "\texample usage:" << endl
		<< name << " -fprop_1" << endl << endl
		<< "================================================" << endl
		<< "\tMode 2. -a{address} - remote address" << endl
		<< "\t-r{remote port} - remote port" << endl
		<< "\t-l{local port} - local port" << endl
		<< "\texample first client usage:" << endl
		<< name << " -alocalhost -r5555 -l5556" << endl
		<< "\texample partner client usage:" << endl
		<< name << " -alocalhost -r5556 -l5555" << endl
		<< "================================================" << endl
		<< "for exit type 'q'" << endl;
}
/**
 * @see Chat#startSender
 */
HANDLE Chat::startSender() {
	HANDLE myHandle1 = (HANDLE)_beginthreadex(NULL, 0, Chat::sender, (LPVOID)this, CREATE_SUSPENDED, &myThreadID1);
	DWORD rc = ResumeThread(myHandle1);
	return myHandle1;
}
/**
 * @see Chat#startReciver
 */
HANDLE Chat::startReciver() {
	myHandle2 =  (HANDLE)_beginthreadex(NULL, 0, Chat::reciver, (LPVOID)this, CREATE_SUSPENDED, &myThreadID2);
	DWORD rc = ResumeThread(myHandle2);
	return myHandle2;
}
/**
 * @see Chat#~Chat
 */
Chat::~Chat() {
	if (myHandle1 != NULL) CloseHandle(myHandle1);
	if (myHandle2 != NULL) CloseHandle(myHandle2);
	delete context;
}
/**
 * @see Chat#reciver
 */
unsigned WINAPI Chat::reciver(LPVOID lpParameter){
	Chat* chat = static_cast<Chat*>(lpParameter);
	// подготовка порта для рассылки сообщений
	socket_t socket (*(chat->context), ZMQ_PULL);
	socket.bind (chat->recive_url.c_str());
	// в цикле ожидаются данные от партнера,
	// которые выводятся на экран
	while (true) {
		try {
			message_t request;
			// ожидание запроса от клиента
			socket.recv (&request);
			string message_string;
			message_string.assign((static_cast<char*>(request.data())), request.size());
			cout << "recived> " << message_string << endl << "query> ";
		} catch (error_t e) {
			cout << "exception " << e.what() << endl;
			if (e.num() == ETERM)
				break;
		}
	}
	return 0;
}
/**
 * @see Chat#sender
 */
unsigned WINAPI Chat::sender(LPVOID lpParameter){
	Chat* chat = static_cast<Chat*>(lpParameter);
	// подготовка порта для рассылки сообщений
	socket_t socket(*(chat->context), ZMQ_PUSH);
	socket.connect(chat->send_url.c_str());
	// в цикле считывается ввод пользователя и сразу же отсылается в порт слушателя
	// выход из цикла (и программы) по вводу "q"
	while (true) {
		try {
			string message;
			cout << "query> ";
			// получить ввод пользователя
			getline(cin, message);
			if (message == "q")
				break;
			message_t request(message.size());
			memcpy((void *)request.data (), message.c_str(), message.size());
			// послать строку в порт
			socket.send(request);
		} catch (error_t e) {
			cout << "exception " << e.what() << endl;
			if (e.num() == ETERM)
				break;
		}
	}
	return 0;
}
/*
 * точка входа в приложение.
 * запуск без параметров выводит подсказку по использованию.
 * выход из программы - ввод строки "q"
 * @param argc количество параметров в командной строке
 * @param argv массив параметров из командной строки
 */
int main (int argc, char* argv[]) {
	auto_ptr<Chat> chat(new Chat(argc, argv));
	if (!chat->checkUrls()) {
		chat->printHelp(argv[0]);
		return 1;
	}
	chat->setUrls();
	HANDLE myHandle1 = chat->startSender();
	HANDLE myHandle2 = chat->startReciver();
	WaitForSingleObject(myHandle1, INFINITE);
	chat.release();
	return 0;
}

